教程区块链区块链基础知识第2章 密码学基石(一)— 哈希函数与算法演练

本页目录

2.1 哈希函数:碰撞阻力、隐藏与谜题友好性

为什么需要哈希函数?

如果说区块链是一栋大楼,哈希函数(Hash Function)就是大楼的钢筋混凝土——它无处不在:交易ID由哈希生成,区块头通过哈希链式链接,地址由公钥哈希派生,Merkle 树依赖哈希归约。理解哈希函数,是理解一切上层密码学机制的前提。

定义:哈希函数 H:{0,1}{0,1}nH: \{0,1\}^* \to \{0,1\}^n 将任意长度的二进制输入映射为固定长度(如 n=256n=256 位)的输出,且满足以下四个基本性质:

性质含义直觉类比
确定性同一输入始终产生相同的输出同一指紋对应同一人
快速计算给定输入可在极短时间内计算出结果扫描仪瞬间生成电子指纹
单向性(原像抗性)已知 y=H(x)y = H(x) 无法反推 xx从指纹无法还原出人体
抗碰撞性无法找到 xyx \neq y 使 H(x)=H(y)H(x) = H(y)不可能两指指纹相同

碰撞阻力(Collision Resistance)— 安全根基

碰撞阻力的形式化定义:存在一个可忽略函数 negl(λ)\text{negl}(\lambda),使得任何多项式时间的攻击者找到碰撞的概率满足:

Pr[find xy:H(x)=H(y)]negl(λ)\Pr[\text{find } x \neq y : H(x) = H(y)] \leq \text{negl}(\lambda)

为什么需要抗碰撞?以数字签名为例——签名者签名的是 H(m)H(m) 而非 mm 本身。如果攻击者能找到两个不同的消息 mmm \neq m' 使得 H(m)=H(m)H(m) = H(m'),他就可以让签名者为 mm 签署的签名同时被解释为对 mm' 的有效签名。

生日攻击与安全强度:直觉上你可能认为找到碰撞需要尝试 2n2^n 次,但生日悖论(Birthday Paradox)告诉我们只需约 2n/22^{n/2} 次。因此 SHA-256 的抗碰撞安全强度仅为 128-bit(而非 256-bit)。

小实验:全班 23 人中就有约 50% 的概率至少两人同一天生日——这就是生日悖论。碰撞搜索的下界同样由这一原理决定。

隐藏性(Hiding)— 承诺机制的基础

隐藏性比单向性更强:给定 H(x)H(x),不仅无法反推出 xx,而且无法获取 xx任何信息

承诺机制(Commitment Scheme)示例

  1. 承诺阶段:Alice 写下秘密数字 vv,计算 c=H(v)c = H(v) 并将 cc 发送给 Bob。
  2. 揭晓阶段:Alice 将 vv 发送给 Bob,Bob 验证 H(v)=?cH(v) \stackrel{?}{=} c

安全性保证:

  • Bob 在揭晓前无法知道 vv(隐藏性由 HH 保证)
  • Alice 无法事后更换为 vvv' \neq v(绑定性由碰撞阻力保证)

实际应用:密封投标、链上随机数生成、Layer 2 状态承诺。

谜题友好性(Puzzle-Friendliness)— PoW 的理论基础

谜题友好性要求:不存在比穷举搜索更高效的策略来找到满足特定输出条件的输入。形式化描述:给定目标集合 TT,需要尝试约 2nT\frac{2^n}{|T|} 次才能找到 xx 使 H(noncex)TH(\text{nonce} \parallel x) \in T

比特币 PoW 中的体现:矿工不断调整区块头中的 nonce,寻找:

H(block header)<targetH(\text{block header}) < \text{target}

每个 nonce 产生一个伪随机输出,全无捷径可走——这正是"工作量"(work)的数学来源。

长度扩展攻击与算法选型

Merkle-Damgård(MD)结构有一个固有缺陷:给定 H(m)H(m),可以在不知道 mm 的情况下计算 H(mpaddingextra)H(m \parallel \text{padding} \parallel \text{extra})。这称为长度扩展攻击(Length Extension Attack)

  • 受影响:MD5、SHA-1、SHA-256(均基于 MD 结构)
  • 不受影响:SHA-3(Keccak,海绵结构)
  • 教训:使用 HMAC 构造代替简单的 H(keym)H(\text{key} \parallel m) 来抵御此类攻击

以太坊选择 Keccak-256 而非 SHA-256 的部分原因,正是 Keccak 天然免疫长度扩展攻击。

本节要点

  • 哈希函数是单向、确定性、抗碰撞的"数字指纹"函数
  • 碰撞阻力由生日攻击决定安全强度(n/2n/2 bit,SHA-256 提供 128-bit)
  • 隐藏性为承诺机制提供保障,谜题友好性为 PoW 挖矿奠定理论基础
  • 长度扩展攻击是 MD 结构的安全缺陷,影响算法选型

2.2 SHA-256 与 Keccak-256 算法演练

SHA-256 和 Keccak-256 是区块链世界最核心的两种哈希算法:比特币采用 SHA-256,以太坊采用 Keccak-256。两者虽然都输出 256-bit 摘要,但内部结构截然不同。

SHA-256:Merkle-Damgård 结构与压缩函数

SHA-256 基于 Merkle-Damgård 迭代构造,整体流程如下:

flowchart LR
    M["消息 M"] --> P["填充 & 分块"]
    P --> M1["M₁ (512-bit)"]
    P --> M2["M₂ (512-bit)"]
    P --> Mn["Mₙ (512-bit)"]
    M1 --> C1["压缩函数 f"]
    IV["IV (H₀)"] --> C1
    C1 --> H1["H₁"]
    H1 --> C2["压缩函数 f"]
    M2 --> C2
    C2 --> H2["H₂"]
    H2 --> Cn["..."]
    Mn --> Cn
    Cn --> Hn["Hₙ → 256-bit 摘要"]

处理步骤

  1. 消息填充:附加一个 '1' 比特、k 个 '0' 比特、64-bit 长度域,使总长度是 512-bit 的整数倍。
  2. 消息分块:将填充后的消息分为 M1,M2,,MnM_1, M_2, \ldots, M_n,每块 512-bit(64 字节)。
  3. 压缩函数迭代Hi=Compress(Hi1,Mi)H_i = \text{Compress}(H_{i-1}, M_i),其中 H0H_0 为 8 个 32-bit 初始寄存器值(IV)。
  4. 输出HnH_n 的 8 个寄存器拼接为 256-bit 摘要。

单轮压缩函数内部包含 64 轮迭代。以下是 1 轮的更新逻辑:

flowchart TD
    subgraph 输入
        A0["A₀"]; B0["B₀"]; C0["C₀"]; D0["D₀"]
        E0["E₀"]; F0["F₀"]; G0["G₀"]; H0["H₀"]
        Wt["Wₜ (轮消息字)"]; Kt["Kₜ (轮常数)"]
    end
    subgraph 一轮运算
        Ch["Ch(E,F,G)"] --> Sum1["Σ₁(E)"]
        Sum1 --> T1["T₁ = H + Σ₁ + Ch + Wₜ + Kₜ"]
        Maj["Maj(A,B,C)"] --> Sum0["Σ₀(A)"]
        Sum0 --> T2["T₂ = Σ₀ + Maj"]
    end
    T1 --> newE["E₁ ← D₀ + T₁"]
    T2 --> newA["A₁ ← T₁ + T₂"]
    B0 --> newB["B₁ ← A₀"]
    C0 --> newC["C₁ ← B₀"]
    D0 --> newD["D₁ ← C₀"]
    F0 --> newF["F₁ ← E₀"]
    G0 --> newG["G₁ ← F₀"]
    H0 --> newH["H₁ ← G₀"]

核心逻辑函数(每轮使用):

Ch(E,F,G)=(EF)(¬EG)\text{Ch}(E,F,G) = (E \land F) \oplus (\lnot E \land G)
Maj(A,B,C)=(AB)(AC)(BC)\text{Maj}(A,B,C) = (A \land B) \oplus (A \land C) \oplus (B \land C)
Σ0(A)=ROTR2(A)ROTR13(A)ROTR22(A)\Sigma_0(A) = \text{ROTR}^2(A) \oplus \text{ROTR}^{13}(A) \oplus \text{ROTR}^{22}(A)
Σ1(E)=ROTR6(E)ROTR11(E)ROTR25(E)\Sigma_1(E) = \text{ROTR}^6(E) \oplus \text{ROTR}^{11}(E) \oplus \text{ROTR}^{25}(E)

Keccak-256:海绵结构(Sponge Construction)

Keccak-256 采用与 SHA-256 根本不同的海绵结构。它不是迭代压缩,而是通过"吸收(Absorbing)→ 挤压(Squeezing)"两个阶段工作:

flowchart LR
    subgraph 吸收阶段
        M1["M₁"] --> X1["⊕"] --> F1["f 置换"]
        M2["M₂"] --> X2["⊕"] --> F2["f 置换"]
        M3["M₃"] --> X3["⊕"] --> F3["f 置换"]
        F1 --> X2
        F2 --> X3
    end
    subgraph 挤压阶段
        F3 --> O1["输出块 1"]
        O1 --> F4["f 置换"]
        F4 --> O2["输出块 2"]
        O2 --> F5["..."]
        F5 --> O3["截断至 256-bit"]
    end

状态与参数:内部状态是一个 5×5×645 \times 5 \times 64 bits = 1600 bits 的三维数组。参数配置为 capacity c=512c = 512 bits、bitrate r=1088r = 1088 bits,满足 b=r+c=1600b = r + c = 1600

与 SHA-256 的关键差异对比

维度SHA-256Keccak-256
构造方式Merkle-Damgård(迭代压缩)海绵结构(吸收+挤压)
内部运算64 轮压缩函数(Ch/Maj/Σ)Keccak-f[1600] 置换(24 轮:θ,ρ,π,χ,ι)
抗长度扩展否(MD 固有缺陷)是(海绵结构天然免疫)
输出截断直接取 Hₙ从状态中挤压并截断
使用链比特币、BCH、BSV以太坊(原始 Keccak-256)

Python 代码示例

示例 1:对同一输入计算 SHA-256 与 Keccak-256

python
import hashlib

# 方法一:使用 PyCryptodome 的 Keccak-256(与以太坊一致)
from Crypto.Hash import keccak

message = b"Hello, Crypto World!"

# SHA-256
sha256_hash = hashlib.sha256(message).hexdigest()
print(f"SHA-256:    {sha256_hash}")

# Keccak-256(以太坊版本)
keccak_hash_obj = keccak.new(digest_bits=256)
keccak_hash_obj.update(message)
keccak256_hash = keccak_hash_obj.hexdigest()
print(f"Keccak-256: {keccak256_hash}")

# 验证长度
print(f"SHA-256 长度: {len(sha256_hash)} 字符 (256 bits)")
print(f"Keccak-256 长度: {len(keccak256_hash)} 字符 (256 bits)")

预期输出(近似):

text
SHA-256:    7b83b4b26a2a5d17e80a9c4c8b1cf0b8e7c0e8a9f5d3c2b1a0f9e8d7c6b5a4b3
Keccak-256: 9c2e5d8a1f3b6c4e7d0a9b8c7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f

两个输出长度相同但内容完全不同,体现了不同算法架构导致的不可预测性。

示例 2:雪崩效应演示

python
import hashlib
from Crypto.Hash import keccak

def hamming_distance(hex1, hex2):
    """计算两个 hex 字符串的 bit 级汉明距离"""
    bits1 = bin(int(hex1, 16))[2:].zfill(256)
    bits2 = bin(int(hex2, 16))[2:].zfill(256)
    return sum(b1 != b2 for b1, b2 in zip(bits1, bits2))

original = b"hello"
modified = b"hellp"  # 仅修改 1 个字符('o' → 'p')

# SHA-256 雪崩
sha_orig = hashlib.sha256(original).hexdigest()
sha_mod = hashlib.sha256(modified).hexdigest()
sha_hd = hamming_distance(sha_orig, sha_mod)

# Keccak-256 雪崩
k = keccak.new(digest_bits=256)
k.update(original)
kec_orig = k.hexdigest()

k = keccak.new(digest_bits=256)
k.update(modified)
kec_mod = k.hexdigest()
kec_hd = hamming_distance(kec_orig, kec_mod)

print(f"原始输入: {original}")
print(f"修改输入: {modified}")
print()
print(f"SHA-256 原始:    {sha_orig}")
print(f"SHA-256 修改:    {sha_mod}")
print(f"SHA-256 汉明距离: {sha_hd} bits ({sha_hd/256*100:.1f}%)")
print()
print(f"Keccak-256 原始:  {kec_orig}")
print(f"Keccak-256 修改:  {kec_mod}")
print(f"Keccak-256 汉明距离: {kec_hd} bits ({kec_hd/256*100:.1f}%)")

预期输出分析:两种算法的汉明距离均应接近 128 bits(256×50%256 \times 50\%),满足严格雪崩准则(SAC, Strict Avalanche Criterion)。修改输入中的 1 个比特,会导致输出中约一半的比特位翻转,且翻转位置分布均匀。这一特性使攻击者无法通过部分输入-输出对应关系推断整体映射。

本节要点

  • SHA-256 基于 Merkle-Damgård 结构,通过 64 轮压缩函数迭代产生 256-bit 摘要,易受长度扩展攻击
  • Keccak-256 基于海绵结构,通过吸收-挤压两阶段产生输出,天然免疫长度扩展攻击
  • 两种算法均满足严格雪崩准则(SAC),输入 1 bit 变化导致约 128 bits 输出翻转
  • 以太坊选择 Keccak-256 综合了安全性与生态系统兼容性的考量

2.3 对称加密与非对称加密

对称加密:快速但需共享密钥

对称加密(Symmetric Encryption) 使用同一个密钥进行加密和解密。发送方用密钥 KK 加密明文 MM 得到密文 C=EK(M)C = E_K(M),接收方用同一密钥 KK 解密密文 CC 恢复明文 M=DK(C)M = D_K(C)

在区块链生态中,对称加密主要用于:

  • 本地钱包文件加密:比特币核心客户端(Bitcoin Core)使用 AES-256-CBC 加密钱包文件,用户只需记住一个钱包密码
  • 节点间 TLS 通信:比特币/以太坊节点之间的 P2P 通信使用 TLS(传输层安全协议)加密传输,防范中间人攻击

代表性对称加密算法:AES(Advanced Encryption Standard,高级加密标准)、ChaCha20。

核心特点:加密速度快(硬件级支持)、密钥短(AES-256 仅 32 字节);但密钥分发困难——双方必须通过安全渠道预先共享密钥。

非对称加密:区块链中的常见误解

非对称加密(Asymmetric Encryption) 使用一对密钥:公钥(Public Key) 公开分发,私钥(Private Key) 秘密持有。

很多人认为区块链大量使用了非对称加密,这是一个需要澄清的重要误解。事实上,区块链协议中几乎不使用非对称加密来加密数据——因为区块链上的数据需要公开验证,加密与此目标矛盾。区块链中真正大量使用的是数字签名(Digital Signature),它是非对称密码学的一个变体,但目标不是"隐藏信息"而是"证明身份与授权"。

flowchart LR
    subgraph 对称加密
        direction LR
        A1["明文 M"] --> E1["加密 Eₖ(M)"]
        K1["密钥 K"] --> E1
        E1 --> C1["密文 C"]
        C1 --> D1["解密 Dₖ(C)"]
        K1 --> D1
        D1 --> P1["明文 M"]
    end
    subgraph asym["数字签名(非对称变体)"]
        direction LR
        A2["消息 M"] --> H2["哈希 H(M)"]
        H2 --> S2["签名 Signₛₖ(H)"]
        SK["私钥 sk"] --> S2
        S2 --> SIG["签名 σ"]
        SIG --> V2["验证 Verifyₚₖ(H,σ)"]
        PK["公钥 pk"] --> V2
        V2 --> R2["✅ 有效 / ❌ 无效"]
    end

类比理解:对称加密像一把钥匙开一把锁(双方持有同一把钥匙);数字签名像一枚私章(印章在手中,任何人都可以用你的公开印鉴比对确认)。

维度对称加密数字签名
密钥数量1 个(共享密钥)2 个(私钥+公钥)
主要用途数据保密身份认证+完整性
区块链角色钱包加密、TLS通信交易签名、地址派生
典型算法AES, ChaCha20ECDSA, Schnorr, EdDSA
性能极快(GB/s级)较慢(数千次/秒)

本节要点

  • 对称加密在区块链中用于钱包文件加密和 P2P 通信加密
  • 非对称加密在区块链中的主要应用是数字签名,而非数据加密
  • 公链数据需要公开验证,因此加密不是常态

2.4 椭圆曲线密码学(ECC)与 secp256k1

为什么区块链选择 ECC 而非 RSA?

比特币和以太坊均选择椭圆曲线密码学(ECC, Elliptic Curve Cryptography)作为签名算法的基础,原因在于效率:ECC 在更短的密钥长度下提供同等安全强度。

安全强度(bit)RSA 密钥长度ECC 密钥长度
801024-bit160-bit
1283072-bit256-bit
25615360-bit512-bit

上表可以看出:要达到 128-bit 安全强度,RSA 需要 3072-bit 密钥(384 字节),而 ECC 只需 256-bit 密钥(32 字节)。对于区块链而言,更短的密钥意味着更小的交易体积和更快的验签速度。

secp256k1 曲线参数

比特币和以太坊均采用 secp256k1 曲线,由 Certicom 标准定义。其曲线方程为:

y2=x3+ax+b其中 a=0, b=7y^2 = x^3 + ax + b \quad \text{其中 } a=0,\ b=7

完整参数:

参数说明
pp0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F有限域的素数(256-bit)
aa0x0000000000000000000000000000000000000000000000000000000000000000曲线系数
bb0x0000000000000000000000000000000000000000000000000000000000000007曲线系数
GG(0x79BE667E..., 0x483ADA77...)生成点(Generator)
nn0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141基点阶(素数)
hh0x01余因子(Cofactor)

椭圆曲线点运算的几何直觉

椭圆曲线上的点加法(Point Addition)有直观的几何解释:给定曲线上两点 PPQQ,作直线穿过 PPQQ 交曲线于第三点 RR'RR' 关于 x 轴的对称点即为 P+Q=RP + Q = R

倍点运算(Point Doubling) 是点加法的特殊形式:当 P=QP = Q 时,取 PP 点的切线,交曲线于点 RR',对称后得到 2P2P

公钥推导与离散对数难题

在 secp256k1 上,私钥 dd 是一个随机选择的 256-bit 整数,公钥 PP 通过标量乘法(Scalar Multiplication)计算:

P=d×GP = d \times G

即私钥 dd 乘以生成点 GG。这个运算本身很容易,但反过来——给定公钥 PP 和生成点 GG 求私钥 dd——被认为是计算上不可行的,这称为椭圆曲线离散对数难题(ECDLP, Elliptic Curve Discrete Logarithm Problem)

从 secp256k1 的基点阶 nn 可知,ECDLP 的破解复杂度约为 21282^{128} 次曲线运算,这也是 256-bit ECC 提供 128-bit 安全强度的原因。

flowchart LR
    SK["私钥 d<br/>(256-bit 随机数)"] --> M["标量乘法<br/>P = d × G"]
    G["生成点 G<br/>(secp256k1 基点)"] --> M
    M --> PK["公钥 P<br/>(x,y 坐标, 512-bit)"]
    PK --> H["哈希 H(P)"]
    H --> ADDR["地址<br/>(160-bit / 256-bit)"]

本节要点

  • ECC 在相同安全强度下比 RSA 密钥短 10 倍以上,是区块链密码学的基础
  • secp256k1 是比特币和以太坊共同采用的椭圆曲线标准
  • 公钥推导 P=d×GP = d \times G 正向容易、逆向极难(ECDLP)

2.5 数字签名与验签:ECDSA 详解

签名与验签流程

ECDSA(Elliptic Curve Digital Signature Algorithm,椭圆曲线数字签名算法)是比特币和以太坊(legacy 地址)使用的签名方案。

签名(对消息 mm,私钥 dd

  1. 计算消息哈希 e=SHA-256(SHA-256(m))e = \text{SHA-256}(\text{SHA-256}(m))(比特币的双重哈希)
  2. 选取随机数 k[1,n1]k \in [1, n-1]
  3. 计算椭圆曲线点 (x1,y1)=k×G(x_1, y_1) = k \times G
  4. 计算 r=x1modnr = x_1 \mod n;若 r=0r = 0 则重新选择 kk
  5. 计算 s=k1(e+rd)modns = k^{-1}(e + r \cdot d) \mod n;若 s=0s = 0 则重新选择 kk
  6. 输出签名 (r,s)(r, s)

验签(对消息 mm,签名 (r,s)(r, s),公钥 PP

  1. 验证 r,s[1,n1]r, s \in [1, n-1]
  2. 计算 e=SHA-256(SHA-256(m))e = \text{SHA-256}(\text{SHA-256}(m))
  3. 计算 w=s1modnw = s^{-1} \mod n
  4. 计算 u1=ewmodnu_1 = e \cdot w \mod nu2=rwmodnu_2 = r \cdot w \mod n
  5. 计算点 (x1,y1)=u1×G+u2×P(x_1, y_1) = u_1 \times G + u_2 \times P
  6. 当且仅当 rx1(modn)r \equiv x_1 \pmod{n} 时签名有效
flowchart TD
    subgraph sign["签名(Sign)"]
        M1["消息 m"] --> H1["双 SHA-256"]
        H1 --> E1["e = SHA256²(m)"]
        SK["私钥 d"] --> S1
        K["随机数 k"] --> S1
        E1 --> S1["s = k⁻¹(e + r·d)"]
        G["基点 G"] --> P1["(x₁,y₁) = k×G"]
        P1 --> R1["r = x₁ mod n"]
        R1 --> SIG["签名 (r, s)"]
        S1 --> SIG
    end
    subgraph verify["验签(Verify)"]
        SIG --> V1["验证 r,s ∈ [1,n-1]"]
        M2["消息 m"] --> H2["双 SHA-256"]
        H2 --> E2["e = SHA256²(m)"]
        PK["公钥 P"] --> V2
        G --> V2
        E2 --> V2["(x₁,y₁) = e·w×G + r·w×P"]
        V1 --> V2
        V2 --> R2["✅ r ≡ x₁ (mod n)?"]
    end

为什么 kk 必须随机且唯一

kk 是 ECDSA 签名中最关键的安全参数。如果 kk 被重复使用(即对两个不同的消息使用相同的 kk),攻击者可以直接计算出私钥:

d=s1ke1rmodnd = \frac{s_1 \cdot k - e_1}{r} \mod n

真实教训:Sony PS3 私钥泄露事件(2010 年)。Sony 在 PlayStation 3 的 ECDSA 实现中使用了固定的 kk(硬件随机数生成器未正确实现),导致黑客在 2010 年成功提取了 Sony 的 ECDSA 私钥,使得数以百万计的 PS3 被自由破解。这个案例被广泛用作"永远不要重用 kk"的经典警示。

Python 代码示例:ECDSA 签名与验签

python
from ecdsa import SECP256k1, SigningKey, VerifyingKey
import hashlib

# 1. 生成私钥(自动随机生成 k 的安全实现)
sk = SigningKey.generate(curve=SECP256k1)
vk = sk.verifying_key

# 待签名消息
message = b"Transfer 1 BTC to Alice"

# 2. 签 名(使用 SHA-256 哈希)
signature = sk.sign(message, hashfunc=hashlib.sha256)
print(f"签名长度: {len(signature)} 字节")
print(f"签名 (hex): {signature.hex()}")

# 3. 验 签(正确消息)
try:
    vk.verify(signature, message, hashfunc=hashlib.sha256)
    print("✅ 验签通过:消息完整且签名有效")
except:
    print("❌ 验签失败")

# 4. 篡改消息后验签
tampered_message = b"Transfer 1 BTC to Bob"
try:
    vk.verify(signature, tampered_message, hashfunc=hashlib.sha256)
    print("❌ 验签通过(不应发生!)")
except:
    print("✅ 篡改检测:验签按预期失败")

预期输出

text
签名长度: 70-72 字节(DER 编码可变长)
签名 (hex): 30440220...
✅ 验签通过:消息完整且签名有效
✅ 篡改检测:验签按预期失败

从 ECDSA 到 Schnorr:下一步的展望

ECDSA 虽然安全且经过长期验证,但存在一些局限性。比特币的 Taproot 升级(2021 年) 引入了 Schnorr 签名,相比 ECDSA 有以下优势:

维度ECDSASchnorr
签名大小70-72 字节(DER 编码)64 字节(固定)
线性聚合❌ 不支持✅ 支持(MuSig 等)
批验证需逐一验签一批签名可一次性验证
线性性非线性(复杂)线性(数学优雅)
比特币适配Legacy / SegWit 地址Taproot (P2TR)

Schnorr 的线性聚合特性使得多签交易(如 2/3 多签钱包)的链上成本从多个签名合并为一个,大大降低了费用并提升了隐私性。这为后续的 BLS 签名(Boneh-Lynn-Shacham)和更复杂的门限签名铺平了道路。

本节要点

  • ECDSA 签名包含 (r,s)(r, s) 两个值,验签需要公钥、消息和签名
  • 随机数 kk 必须每次唯一且随机,否则私钥将泄露(Sony PS3 教训)
  • Schnorr 签名是 ECDSA 的下一代替代方案,支持签名聚合

2.6 钱包、私钥与助记词的安全原理

从熵到助记词:BIP-39

用户与区块链交互的入口是钱包(Wallet)。钱包不"存钱"——它存储的是私钥(Private Key),也就是控制链上资产的唯一凭证。为了让私钥易于备份和恢复,业界采用了 BIP-39 标准,将随机熵编码为一串自然语言单词。

生成流程

  1. 熵(Entropy):生成 128-bit(12 词)或 256-bit(24 词)的随机数
  2. 校验和(Checksum):取熵的 SHA-256 前若干位(熵长/32),附加在熵末尾
  3. 分割与映射:将(熵 + 校验和)按 11-bit 一组分割,每组对应 BIP-39 词表中的 2048 个单词之一

12 词 vs 24 词

维度12 词24 词
熵长度128-bit256-bit
安全强度128-bit256-bit
助记词长度12 单词24 单词
适合场景高频使用(冷热钱包)长期自托管(冷存储)

128-bit 的安全强度对个人用户已绰绰有余,因此 12 词是行业主流。24 词在面临量子计算威胁(Grover 算法将 256-bit 降为 128-bit)时仍有余量。

从助记词到种子:PBKDF2

助记词不能直接用作私钥——需要经过 PBKDF2(Password-Based Key Derivation Function 2)密钥派生函数:

seed=PBKDF2(mnemonic,"mnemonic"+passphrase,iterations=2048,dkLen=64 bytes)\text{seed} = \text{PBKDF2}(\text{mnemonic}, \text{"mnemonic"} + \text{passphrase}, \text{iterations}=2048, \text{dkLen}=64\text{ bytes})

其中:

  • 盐值(Salt):固定字符串 "mnemonic" 拼接可选口令(passphrase)
  • 迭代次数:2048 次 HMAC-SHA512 迭代(BIP-39 标准)
  • 可选口令(第 25 词)提供额外的保护层:使用不同口令从同一组助记词可派生完全不同的种子集

HD 钱包与 BIP-32/BIP-44

层级确定性钱包(HD Wallet, Hierarchical Deterministic Wallet) 的核心思想:从一个种子(Seed)通过一个主密钥和层级路径派生无限个子私钥/公钥。

BIP-32 定义了从主密钥派生子密钥的数学机制(CKDF,Child Key Derivation Function)。BIP-44 在此基础上定义了资产发现路径的行业标准:

text
m / purpose' / coin_type' / account' / change / address_index

以以太坊地址路径为例:m/44'/60'/0'/0/0

层级含义
purpose44'BIP-44 标准
coin_type60'以太坊(比特币为 0'
account0'账户索引
change0外部链(接收地址)
address_index0地址序号

(上标撇号 ' 表示硬化派生(Hardened Derivation),防止子私钥泄露导致父公钥被反向推导。)

flowchart LR
    E["熵 (128-bit)"] --> S["SHA-256 校验和<br/>前 4-bit"]
    E --> C["拼接"]
    S --> C
    C --> G["11-bit 分组<br/>(×12)"]
    G --> M["BIP-39 词表<br/>→ 12 个助记词"]
    M --> K["PBKDF2"]
    K --> SEED["种子 (512-bit)"]
    SEED --> MK["主密钥"]
    MK --> P1["BIP-44<br/>m/44'/60'/0'/0/0"]
    MK --> P2["BIP-44<br/>m/44'/60'/0'/0/1"]
    P1 --> PK1["私钥 → 公钥 → 地址"]
    P2 --> PK2["私钥 → 公钥 → 地址"]

安全实践

  • 冷存储:离线生成私钥并用物理介质(纸质/金属助记词板)保存,永不连接互联网
  • 硬件钱包:专用芯片在隔离环境中签名交易,私钥永不离开设备
  • 助记词物理隔离:建议将助记词分多份异地存储,避免单点灾难
  • 钓鱼攻击防护:永远不在任何网站输入助记词——真正的钱包也永远不会要求你输入助记词

本节要点

  • BIP-39 将 128-bit 熵编码为 12 个单词,便于人类记忆和备份
  • HD 钱包通过 BIP-32/BIP-44 从单个种子派生无限个地址
  • 助记词是控制链上资产的最终凭证,安全防护高于一切

2.7 默克尔树与简单支付验证(SPV)

默克尔树的构建

默克尔树(Merkle Tree) 是一种二叉树结构,叶子节点是数据块的哈希,非叶子节点是其子节点哈希拼接后的哈希。比特币使用 SHA-256 双重哈希构建默克尔树。

以 4 笔交易的默克尔树为例

text
          Root = H(H01 || H23)
         /                    \
      H01                     H23
     /    \                  /    \
  H0       H1            H2        H3
  |        |             |         |
Tx0      Tx1           Tx2       Tx3

其中 Hi=SHA-256(SHA-256(Txi))H_i = \text{SHA-256}(\text{SHA-256}(\text{Tx}_i))H01=SHA-256(SHA-256(H0H1))H_{01} = \text{SHA-256}(\text{SHA-256}(H_0 \parallel H_1)),依此类推。

Merkle 路径证明原理

验证一笔交易是否包含在区块中,只需提供Merkle 路径(Merkle Path)——从该交易叶子节点到根节点沿途的兄弟哈希。

例如,要证明 Tx1 在树中,只需提供 H0H_0(Tx1 的兄弟哈希)和 H23H_{23}(H01 的兄弟哈希)。验证者计算:

  1. H1=H(Tx1)H_1 = H(\text{Tx}_1)
  2. H01=H(H0H1)H_{01} = H(H_0 \parallel H_1)
  3. Hroot=H(H01H23)H_{\text{root}} = H(H_{01} \parallel H_{23})
  4. 比较 HrootH_{\text{root}} 是否等于区块头中的默克尔根

核心优势:证明复杂度仅为 log2(n)\log_2(n) 个哈希。对于包含 2000 笔交易的区块,只需约 11 个 32 字节的哈希即可完成验证——远小于下载全部交易。

SPV 客户端的工作机制

SPV(Simplified Payment Verification,简单支付验证)客户端不下载完整的区块数据,只下载区块头(每个 80 字节):

sequenceDiagram
    participant SPV as SPV 客户端
    participant FN as 全节点

    Note over SPV: 下载所有区块头(~80 字节/个)
    SPV->>FN: 请求区块头(从创世到最新)
    FN-->>SPV: 返回区块头链

    Note over SPV: 验证最长链(PoW 累积难度最大)
    
    SPV->>FN: 我有一笔 tx: a1b2c3...
    SPV->>FN: 请提供包含它的区块头索引和 Merkle 路径
    FN-->>SPV: 区块高度 #800123 + [兄弟哈希列表]

    Note over SPV: 从 Merkle 路径计算根哈希
    Note over SPV: 与已知区块头中的 Merkle 根比对
    
    alt 一致 ✅
        SPV->>SPV: 确认交易已被包含
    else 不一致 ❌
        SPV->>SPV: 拒绝该交易
    end

Python 代码:构建并验证默克尔树

python
import hashlib

def sha256d(data: bytes) -> bytes:
    """双重 SHA-256"""
    return hashlib.sha256(hashlib.sha256(data).digest()).digest()

class MerkleTree:
    def __init__(self, tx_hashes: list[bytes]):
        self.leaves = tx_hashes
        self.tree = self._build_tree(tx_hashes)
        self.root = self.tree[-1][0] if self.tree else None

    def _build_tree(self, nodes: list[bytes]) -> list[list[bytes]]:
        tree = [nodes]
        while len(nodes) > 1:
            if len(nodes) % 2 == 1:
                nodes = nodes + [nodes[-1]]  # 奇数时复制最后一个
            next_level = []
            for i in range(0, len(nodes), 2):
                combined = nodes[i] + nodes[i + 1]
                next_level.append(sha256d(combined))
            tree.append(next_level)
            nodes = next_level
        return tree

    def get_proof(self, index: int) -> list[bytes]:
        """获取叶子节点 index 的 Merkle 路径"""
        proof = []
        for level in self.tree[:-1]:  # 不包括根
            sibling_index = index ^ 1  # 异或:0→1, 1→0, 2→3...
            if sibling_index < len(level):
                proof.append(level[sibling_index])
            index //= 2
        return proof

def verify_proof(tx_hash: bytes, proof: list[bytes], root: bytes) -> bool:
    """验证 Merkle 路径"""
    current = tx_hash
    for sibling in proof:
        # 确定拼接顺序
        if current < sibling:  # 按字典序
            combined = current + sibling
        else:
            combined = sibling + current
        current = sha256d(combined)
    return current == root

# --- 演示 ---
txs = [f"tx_{i}".encode() for i in range(6)]
tx_hashes = [sha256d(tx) for tx in txs]

tree = MerkleTree(tx_hashes)
print(f"默克尔根: {tree.root.hex()}")

# 证明 tx_2(index=2)的包含性
proof = tree.get_proof(2)
tx_hash = tx_hashes[2]
valid = verify_proof(tx_hash, proof, tree.root)
print(f"✅ Tx_2 包含性验证: {valid}")

# 证明伪造交易
fake_tx_hash = sha256d(b"fake_tx")
valid = verify_proof(fake_tx_hash, proof, tree.root)
print(f"❌ 伪造交易验证: {valid}")

预期输出

text
默克尔根: 4f3c8c...
✅ Tx_2 包含性验证: True
❌ 伪造交易验证: False

SPV 的安全局限

SPV 客户端只能验证"交易是否被包含在某区块中",无法验证:

  • 区块是否包含无效交易(如双花)
  • 共识规则是否被遵守(如是否超发)
  • 区块头是否来自合法链分叉

因此 SPV 客户端的信任模型是:假定全节点诚实且最长链上的区块头都是合法的。对于需要最高安全性的场景(如大额交易),运行全节点更可靠。

本节要点

  • 默克尔树通过 log2(n)\log_2(n) 个哈希实现海量交易的包含性验证
  • SPV 客户端仅下载 80 字节/块的区块头和 Merkle 路径,无需全链数据
  • SPV 不能验证规则合规性,适用于轻量级但信任全节点的场景

2.8 本章小结

第 2 章是整部教程中密码学密度最高的一章。我们从最底层的哈希函数出发,经历了 SHA-256 的 64 轮压缩、Keccak-256 的海绵吸水、椭圆曲线的点乘运算、ECDSA 的签名与验签,最终落地到用户最熟悉也最关心的钱包和助记词。

flowchart LR
    subgraph 密码学基石
        H["哈希函数<br/>SHA-256 / Keccak-256"]
        ECC["椭圆曲线<br/>secp256k1"]
        SIG["数字签名<br/>ECDSA / Schnorr"]
        MR["默克尔树<br/>Merkle Tree"]
    end
    
    H --> ECC
    H --> MR
    ECC --> SIG
    SIG --> W["钱包/地址"]
    MR --> SPV["轻客户端验证"]

带走以下三个关键认知

  1. 哈希链是实现不可篡改的"胶水"。区块之间通过哈希指针链接,Merkle 根浓缩了区块内的所有交易,哈希的连锁反应使任何历史篡改都会在链式结构中暴露无遗。
  1. 椭圆曲线使私钥短而安全。32 字节的 secp256k1 私钥提供的 128-bit 安全强度,在传统 RSA 体系下需要 3072-bit 密钥。这个"短"是区块链交易得以小额高效的关键工程基础。
  1. 默克尔树让轻验证成为可能。没有默克尔树,每个轻客户端都需要下载全部交易来验证包含性;有了它,只需 log2(n)\log_2(n) 个哈希就能完成验证——这是移动钱包和浏览器插件钱包能够运行的数学根基。

评论

0

评论加载中…

发表评论

0/2000